EB3I n1 2025 scRNAseq
-
PRE-PROCESSING (III)
-
Filtering, cell cyle, cell doublets
EB3I n1 2025 scRNAseq
-
PRE-PROCESSING (III)
-
Filtering, cell cyle, cell doublets
1 PREAMBLE
1.1 Purpose of this session
This file describes the different steps to perform the third part of the single cell RNAseq data analysis training course for the EB3I n1 2025, covering these steps :
Filtering of bad cells and extreme low-expression features
Estimation of the cell cycle phase
Identification and removal of cell doublets
2 Start Rstudio
- Using the OpenOnDemand/Rstudio cheat sheet, connect to the OpenOnDemand portal and create a Rstudio session with the right resource requirements.
3 Warm-up
- We set common parameters we will use throughout this session :
# setparam
## Set your project name
# WARNING : Do not just copy-paste this ! It's MY project name ! Put YOURS !!
project_name <- "ebaii_sc_teachers"
## Control if the project_name exists on the cluster
cat('PATH CHECK : ', dir.exists(paste0('/shared/projects/', project_name)))Show output
PATH CHECK : TRUE
4 Prepare the data structure
We will do the same as for former steps, just changing the session name.
4.1 Main directory
# maindir
## Preparing the path
TD_dir <- paste0("/shared/projects/", project_name, "/SC_TD")
## Creating the root directory
# dir.create(path = TD_dir, recursive = TRUE)
## Print the root directory on-screen
print(TD_dir)[1] "/shared/projects/ebaii_sc_teachers/SC_TD"
4.2 Current session
# sessiondir
## Creating the session (Preproc.3) directory
session_dir <- paste0(TD_dir, "/03_Preproc.3")
dir.create(path = session_dir, recursive = TRUE)
## Print the session directory on-screen
print(session_dir)[1] "/shared/projects/ebaii_sc_teachers/SC_TD/03_Preproc.3"
4.3 Input directory
# indir
## Creating the INPUT data directory
input_dir <- paste0(session_dir, "/DATA")
dir.create(path = input_dir, recursive = TRUE)
## Print the input directory on-screen
print(input_dir)[1] "/shared/projects/ebaii_sc_teachers/SC_TD/03_Preproc.3/DATA"
4.4 Genelists directory
This is a directory where we will store additional information from knowledge bases about genes used to estimate the cell cycle phase of cells.
# resdir
res_dir <- paste0(TD_dir, "/Resources")
glist_dir <- paste0(res_dir, "/Genelists")
## Create the directory
dir.create(path = glist_dir, recursive = TRUE)
## Print the resources directory on-screen
print(glist_dir)[1] "/shared/projects/ebaii_sc_teachers/SC_TD/Resources/Genelists"
5 Reload the Seurat Object
- We can reload the object we saved at the former step
# dataload
## The latest Seurat object saved as RDS (name)
sobj_file <- "02_TD3A_S5_Metrics_31053.4587.RDS"
## The latest Seurat object saved as RDS (full path)
sobj_path <- paste0(TD_dir,
"/02_Preproc.2/RESULTS/",
sobj_file)
force <- FALSE ## To force a re-download of a Zenodo-hosted backup
local <- FALSE ## To force a loading from a local backup
## In case of error/lost data : force a reload from a Zenodo backup repository
if(force) {
zen_id <- "14035293"
zen_backup_file <- paste0("https://zenodo.org/records/",
zen_id,
"/files/",
sobj_file)
## Recreate the expected path if it does not exist
dir.create(path = dirname(sobj_path), recursive = TRUE)
## Download the file
download.file(url = zen_backup_file,
destfile = sobj_path)
}
## In case of error/lost data : force a reload from a local backup repository
if(local) {
sobj_path <- paste0(
"/shared/projects/2422_ebaii_n1/atelier_scrnaseq/TD/BACKUP/RDS/",
sobj_file)
}
## Load the object
sobj <- readRDS(file = sobj_path)Now, we finally have all the metrics and bias sources we could use, so we can actually get to the filtering step.
6 Filtering
6.1 Features
As already explained, the sparsity of the count matrix is high, very high.
To the point that there probably are some features that have so low expression level that they were only counted in very few cells. As such, they have absolutely no chance to characterize any cell type, nor harbor some variation between different cell types.
As such, we should discard these features
What are the actual dimensions of our expression data ?
Show output
[1] 31053 4587
We want to remove the features that are expressed in less than 5 cells.
First, we quantify, for each feature, the number of cells with 0 count.
# nfeatdiff
## Compute the amount of 0s per feature
nFeat_zero <- sparseMatrixStats::rowCounts(
x = SeuratObject::GetAssayData(
object = sobj,
assay = "RNA",
layer = "counts"),
value = 0)
## Inversion : computing the number of cells with at least one count, per feature
nFeat_nonzero <- ncol(sobj) - nFeat_zero
## Identify those with at least 5 cells with expression
nFeat_keep <- nFeat_nonzero >= min_cells
## Quantify the selected ones
table(nFeat_keep)Show output
nFeat_keep
FALSE TRUE
18545 12508
And now we can discard features expressed in less than 5 cells :
# prefeatfiltviz
## Create a temporary object
sobj_VIZ_pre <- sobj
## Usual set of "U can't C me" functions
sobj_VIZ_pre <- Seurat::NormalizeData(object = sobj_VIZ_pre, verbose = FALSE)
sobj_VIZ_pre <- Seurat::ScaleData(object = sobj_VIZ_pre, verbose = FALSE)
sobj_VIZ_pre <- Seurat::FindVariableFeatures(object = sobj_VIZ_pre, verbose = FALSE)
sobj_VIZ_pre <- Seurat::RunPCA(object = sobj_VIZ_pre, npcs = 21, verbose = FALSE)
sobj_VIZ_pre <- Seurat::RunUMAP(object = sobj_VIZ_pre, dims = c(1:20), verbose = FALSE)
## Cell space visualization
dp_pre <- Seurat::DimPlot(
object = sobj_VIZ_pre,
reduction = 'umap') + Seurat::DarkTheme()
print(dp_pre)Show plot
# featsel
## Restrict the Seurat object
sobj <- subset(x = sobj, features = rownames(sobj)[nFeat_keep])What are the Seurat object dimensions, now ?
Show output
[1] 12508 4587
We can visualize the cell space after this features filtering
# postfeatfiltviz
## Create a temporary object
sobj_VIZ_post <- sobj
## Usual set of "U can't C me" functions
sobj_VIZ_post <- Seurat::NormalizeData(object = sobj_VIZ_post, verbose = FALSE)
sobj_VIZ_post <- Seurat::ScaleData(object = sobj_VIZ_post, verbose = FALSE)
sobj_VIZ_post <- Seurat::FindVariableFeatures(object = sobj_VIZ_post, verbose = FALSE)
sobj_VIZ_post <- Seurat::RunPCA(object = sobj_VIZ_post, npcs = 21, verbose = FALSE)
sobj_VIZ_post <- Seurat::RunUMAP(object = sobj_VIZ_post, dims = c(1:20), verbose = FALSE)
## Cell space visualization
dp_post <- Seurat::DimPlot(
object = sobj_VIZ_post,
reduction = 'umap') + Seurat::DarkTheme()
print(dp_post)Show plot
Merging plots for ease of visualization :
# c5_pw
## Using the patchwork package to merge plots (and ggplot2 to add titles)
patchwork::wrap_plots(
list(
dp_pre & ggplot2::ggtitle(label = "BEFORE"),
dp_post & ggplot2::ggtitle(label = "AFTER")),
nrow = 1)Show plot
Question :
6.2 Cells
We are now able to apply all the filtering strategies we established for each QC metric.
Apply the filter
Show output
[1] 12508 4587## Apply cell filtering sobj <- subset(x = sobj, cells = colnames(sobj)[sobj$fail_qc == "pass"]) ## Seurat object AFTER cell filtering dim(sobj)Show output
[1] 12508 4278
We can visualize the cell space since this cell filtering
# postcellfiltviz
## Create a temporary object
sobj_VIZ <- sobj
## Usual set of "U can't C me" functions
sobj_VIZ <- Seurat::NormalizeData(object = sobj_VIZ, verbose = FALSE)
sobj_VIZ <- Seurat::ScaleData(object = sobj_VIZ, verbose = FALSE)
sobj_VIZ <- Seurat::FindVariableFeatures(object = sobj_VIZ, verbose = FALSE)
sobj_VIZ <- Seurat::RunPCA(object = sobj_VIZ, npcs = 21, verbose = FALSE)
sobj_VIZ <- Seurat::RunUMAP(object = sobj_VIZ, dims = c(1:20), verbose = FALSE)
## Cell space visualization
dp_VIZ <- Seurat::DimPlot(
object = sobj_VIZ,
reduction = 'umap') + Seurat::DarkTheme()
print(dp_VIZ)Show plot
One can associate the visualization after filtering cells :
# cfilt_umaps
## Using the patchwork package to merge plots (and ggplot2 to add titles)
patchwork::wrap_plots(
list(
dp_post & ggplot2::ggtitle(label = "Features (count<5) filtered"),
dp_VIZ & ggplot2::ggtitle(label = "Cells filtered (metrics)")),
nrow = 1)Show plot
- Question
```{.r .question}
# q_vizfilt
Do you see any difference when comparing before vs after features filtering ?
```
<br>
```{.r .fold-hide .answer}
# q_postcellfilt
## . Not much changed as well (we did not discard many cells)
##
## . The biggest cluster structure seems more defined
```
<br>
7 Save the Seurat object
We will save our Seurat object that now contains filtered cells and features :
# saverds1
## Save our Seurat object (rich naming)
out_name <- paste0(
output_dir, "/", paste(
c("03", Seurat::Project(sobj), "S5",
"Metrics.Filtered", paste(
dim(sobj),
collapse = '.'
)
), collapse = "_"),
".RDS")
## Check
print(out_name)[1] "/shared/projects/ebaii_sc_teachers/SC_TD/03_Preproc.3/RESULTS/03_TD3A_S5_Metrics.Filtered_12508.4278.RDS"
8 Cell cycle scores
We are currently analyzing independent profiles from isolated cells, from a sample dissociation
As such, cells were most probably not synchronized, thus the effect of their cell cycle state on genes expression may be strong, to the point that it can bias the data (ie, mask some lower amplitude biological variation).
In order to assess (and maybe, remove) this bias, we have to quantify it.
We will perform this estimation thanks to heuristics based on knowledge : Seurat includes a method that evaluates the cell cycle phase of cells through scores for the S and G2M phases, each based on phase-specific gene signatures.
For this step, we will use additional gene lists from knowledge (cell cycle phase), hosted in a Zenodo respository (Id : 14037355)
8.1 Download gene lists
We will directly retrieve data from Zenodo to your
input_dir:# dlzengl ## Zenodo ID zen_id <- '14101506' ### Named files (will be used later on !) cc_file <- "mus_musculus_Seurat_cc.genes_20191031.rds" ## Filename(s) to retrieve toget_files <- c(cc_file) ## Folder to store retrieved files local_folder <- glist_dir ## Use local backup ? backup <- FALSE if(backup) message("Using local backup !") ## Force download ? force <- FALSE if(force) message("Forcing (re)download !") ### Define remote folder remote_folder <- if (backup) { "/shared/projects/2422_ebaii_n1/atelier_scrnaseq/TD/RESOURCES/GENELISTS/" } else { paste0("https://zenodo.org/records/", zen_id, "/files/") } ### Reconstruct the input paths remote_path <- paste0(remote_folder, "/", toget_files) ### Reconstruct the output paths local_path <- paste0(local_folder, "/", toget_files) ## Retrieve files (if they don't exist), in loop for (tg in seq_along(toget_files)) { ## If the file does not locally exist if (!file.exists(local_path[tg]) | force) { ## Retrieve data if(backup) { file.copy(from = remote_path[tg], to = local_path[tg]) } else { download.file(url = remote_path[tg], destfile = local_path[tg]) } ## Check if downloaded files exist locally if(file.exists(local_path[tg])) message("\tOK") } else message(paste0(toget_files[tg], " already downloaded !")) }
8.2 Load gene lists
# cc_load
## The cell cycle gene lists file
cc_file <- paste0(glist_dir,
"/",
cc_file)
## Load the cell cycle reference genes lists
cc_genes <- readRDS(file = cc_file)
## Have a look on its content
str(cc_genes)Show output
List of 2
$ s.genes : chr [1:43] "Mcl5" "Pcna" "Tyms" "Fen1" ...
$ g2m.genes: chr [1:54] "Hmgb2" "Cdk1" "Nusap1" "Ube2c" ...
Question
# q_cc_check As explained, we will use gene lists extracted from community knowledge. Our data contain values for genes also, so we will cross them. Is there something we should check ?# a_cc_check ## . We may check if the genes in our gene lists are effectively ## present in our Seurat object ! ## ## . This is expected, as we removed features with almost no expression
Beyond :
Write a code that performs this check
Add a code to adjust the content of the gene lists accordingly (remove genes from the gene lists that are not present in our dataset)
(NOTE : this is actually not needed as already checked and corrected by the cell cyle estimation method we will use)
# b_cc_answer
## Check if our data genes cover the gene lists
lapply(cc_genes, function(x) x %in% rownames(sobj))
## Remove genes not in sobj
cc_genes <- lapply(cc_genes, function(gl) { gl[gl %in% rownames(sobj)] })
## Check the modification
str(cc_genes)
## Check that all genes are available in sobj
all(unique(unlist(cc_genes)) %in% rownames(sobj))8.3 Estimation
Let’s perform this estimation. But how ?
Run the cell-cycle estimation
# cc_run ## Creating a temporary object sobj_CCS <- sobj sobj_CCS <- Seurat::NormalizeData(object = sobj_CCS, verbose = FALSE) ## Perform the estimation set.seed(my_seed) sobj_CCS <- Seurat::CellCycleScoring( object = sobj_CCS, s.features = cc_genes$s.genes, g2m.features = cc_genes$g2m.genes, # assay = 'RNA, ## Seurat v4 layer = 'RNA', ## Seurat v5 nbin = 24, seed = my_seed) ## Transfering score from the temp object to the real one sobj$CC_Seurat_S.Score <- sobj_CCS$S.Score sobj$CC_Seurat_G2M.Score <- sobj_CCS$G2M.Score sobj$CC_Seurat_Phase <- as.factor(sobj_CCS$Phase) ## Add "S minus G2M" score sobj$CC_Seurat_SmG2M.Score <- sobj$CC_Seurat_S.Score - sobj$CC_Seurat_G2M.Score ## Discard temp object rm(sobj_CCS)
Description of the object to see the data added
8.4 Visualization
As usual, we can visualize the results as violins :
#vlncc
Seurat::VlnPlot(object = sobj,
features = c("CC_Seurat_S.Score",
"CC_Seurat_G2M.Score",
"CC_Seurat_SmG2M.Score"))Show plot
But it’s not that easy to interpret…
Let’s plot it in the cell space
8.4.1 Estimated cell phase :
# vizccp
## Creating a temporary object.
## This time we will keep it to speed up further plots (next chunks)
sobj_VIZ_cc <- sobj
## Usual set of "U can't C me" functions
sobj_VIZ_cc <- Seurat::NormalizeData(object = sobj_VIZ_cc, verbose = FALSE)
sobj_VIZ_cc <- Seurat::ScaleData(object = sobj_VIZ_cc, verbose = FALSE)
sobj_VIZ_cc <- Seurat::FindVariableFeatures(object = sobj_VIZ_cc, verbose = FALSE)
sobj_VIZ_cc <- Seurat::RunPCA(object = sobj_VIZ_cc, npcs = 21, verbose = FALSE)
sobj_VIZ_cc <- Seurat::RunUMAP(object = sobj_VIZ_cc, dims = c(1:20), verbose = FALSE)
## Cell cycle phase feature plot
dp_ccp <- Seurat::DimPlot(
object = sobj_VIZ_cc,
reduction = 'umap',
group.by = 'CC_Seurat_Phase') + Seurat::DarkTheme()
print(dp_ccp)Show plot
8.4.2 SmG2M (“S minus G2M”) score :
# vizccs
## SmG2M feature plot
fp_cc <- Seurat::FeaturePlot(
object = sobj_VIZ_cc,
reduction = 'umap',
features = 'CC_Seurat_SmG2M.Score') + Seurat::DarkTheme()
print(fp_cc)Show plot
## {.unnumbered}
Questions
# q_cell1
Does the structure of the cells in this representation seem to have
a link with these cell cycle phases/scores ?9 Save the Seurat object
We will save our Seurat object that now contains the cell cycle states/scores :
# saverds2
## Save our Seurat object (rich naming)
out_name <- paste0(
output_dir, "/", paste(
c("04", Seurat::Project(sobj), "S5",
"CC", paste(
dim(sobj),
collapse = '.'
)
), collapse = "_"),
".RDS")
## Check
print(out_name)[1] "/shared/projects/ebaii_sc_teachers/SC_TD/03_Preproc.3/RESULTS/04_TD3A_S5_CC_12508.4278.RDS"
10 Cell doublets
We will use two different methods to detect and remove cell doublets :
scds(in its “hybrid” mode) : more efficient at detecting homotypic doubletsscDblFinder: more efficient at detecting heterotypic doublets
None of the methods accepts a SeuratObject as input, but a SingleCellExperiment object.
Hopefully :
- Seurat has a function to perform the conversion
- The output results can be integrated into our Seurat object with ease
10.1 Two methods
10.1.1 scds
# scds
## Fix seed
set.seed(my_seed)
## Run scds
sobj$doublet_scds.hybrid <- unname(
scds::cxds_bcds_hybrid(
Seurat::as.SingleCellExperiment(
sobj, assay = "RNA"))$hybrid_score > 1)
## Contingencies
table(sobj$doublet_scds.hybrid)Show output
FALSE TRUE
4072 206
10.1.2 scDblFinder
# scdbl
## Fix seed
set.seed(my_seed)
## Run scDblFinder (which needs another object type)
sobj$doublet_scDblFinder <- scDblFinder::scDblFinder(
sce = Seurat::as.SingleCellExperiment(
x = sobj,
assay = "RNA"),
returnType = "table")$class == "doublet"
## Contingencies
table(sobj$doublet_scDblFinder)Show output
FALSE TRUE
4100 178
10.2 Merge results
We merge results of the two methods
# dblmerge
## Logical union of both methods
sobj$doublet_union <- sobj$doublet_scds.hybrid | sobj$doublet_scDblFinder
## Quantify doublets
table(sobj$doublet_union)Show output
FALSE TRUE
4035 243
We can assess tool-specific and common doublets
# dbl_types
### Singlets by default
sobj$doublet_viz <- "singlet"
### Union
sobj$doublet_viz[sobj$doublet_union] <- "both"
### scds-specific
sobj$doublet_viz[sobj$doublet_scds.hybrid & !sobj$doublet_scDblFinder] <- "scds"
### scDblFinder-specific
sobj$doublet_viz[sobj$doublet_scDblFinder & !sobj$doublet_scds.hybrid] <- "scDblFinder"
## Convert to factor
sobj$doublet_viz <- as.factor(sobj$doublet_viz)
## Contingencies
table(sobj$doublet_viz)Show output
both scDblFinder scds singlet
141 37 65 4035
Beyond : Create an upset-plot for the doublet status according to the two methods used
# b_upset
## Build a list of cells tagged by one tool, the other, or both
dbl_list <-list(
"scds" = colnames(sobj)[sobj$doublet_scds.hybrid & !sobj$doublet_scDblFinder],
"scDblFinder" = colnames(sobj)[!sobj$doublet_scds.hybrid & sobj$doublet_scDblFinder],
"both" = colnames(sobj)[sobj$doublet_scds.hybrid & sobj$doublet_scDblFinder])
## Draw the upset plot
UpSetR::upset(data = UpSetR::fromList(dbl_list),
nintersects = NA,
sets = rev(names(dbl_list)),
keep.order = TRUE,
order.by = "freq")Show plot
Now we can remove barcodes identified as cell doublets, and visualize the cell space before and after.
10.3 Doublets filtering
10.3.1 BEFORE
# dblviz1
### Doublets viz (before removal)
## Create a temporary object
sobj_VIZ_dbl <- sobj
## Usual set of "U can't C me" functions
sobj_VIZ_dbl <- Seurat::NormalizeData(object = sobj_VIZ_dbl, verbose = FALSE)
sobj_VIZ_dbl <- Seurat::ScaleData(object = sobj_VIZ_dbl, verbose = FALSE)
sobj_VIZ_dbl <- Seurat::FindVariableFeatures(object = sobj_VIZ_dbl, verbose = FALSE)
sobj_VIZ_dbl <- Seurat::RunPCA(object = sobj_VIZ_dbl, npcs = 21, verbose = FALSE)
sobj_VIZ_dbl <- Seurat::RunUMAP(object = sobj_VIZ_dbl, dims = c(1:20), verbose = FALSE)
## Doublets visualization
dp_dbl <- Seurat::DimPlot(
object = sobj_VIZ_dbl,
reduction = 'umap',
group.by = 'doublet_viz') + Seurat::DarkTheme()
## Plot
print(dp_dbl)Show plot
10.3.2 Remove doublets
[1] 12508 4278
## Perform the removal
# sobj <- sobj[,!sobj$doublet_union]
sobj <- subset(
x = sobj,
cells = colnames(sobj)[sobj$doublet_union],
invert = TRUE)
## Dimensions after
dim(sobj)[1] 12508 4035
10.3.3 AFTER
# dblviz2
### Doublets viz (after removal)
## Create a temporary object
sobj_VIZ_dblf <- sobj
## Usual set of "U can't C me" functions
sobj_VIZ_dblf <- Seurat::NormalizeData(object = sobj_VIZ_dblf, verbose = FALSE)
sobj_VIZ_dblf <- Seurat::ScaleData(object = sobj_VIZ_dblf, verbose = FALSE)
sobj_VIZ_dblf <- Seurat::FindVariableFeatures(object = sobj_VIZ_dblf, verbose = FALSE)
sobj_VIZ_dblf <- Seurat::RunPCA(object = sobj_VIZ_dblf, npcs = 21, verbose = FALSE)
sobj_VIZ_dblf <- Seurat::RunUMAP(object = sobj_VIZ_dblf, dims = c(1:20), verbose = FALSE)
## Doublets-filtered visualization
dp_dblf <- Seurat::DimPlot(
object = sobj_VIZ_dblf,
reduction = 'umap',
group.by = 'doublet_viz') + Seurat::DarkTheme()
## Plot
print(dp_dblf)Show plot
Merge plots for ease of use :
# dbl_umaps
## Using the patchwork package to merge plots (and ggplot2 to add titles)
patchwork::wrap_plots(
list(
dp_dbl & ggplot2::ggtitle(label = "Cell doublets (unfiltered)"),
dp_dblf & ggplot2::ggtitle(label = "Cell doublets (filtered)")),
nrow = 1)Show plot
Question
# a_dblcomp
## . Actually not that much !
##
## . This is due to the fact that this dataset did not contain
## a lot of heterotypic doublets
##
## . When a dataset is heavily contaminated with lots of heterotypic
## doublets, their removal can drastically remodel the topology.11 Save the Seurat object
We will save our Seurat object that is now filtered for doublets :
# saverds3
## Save our Seurat object (rich naming)
out_name <- paste0(
output_dir, "/", paste(
c("05", Seurat::Project(sobj), "S5",
"Doublets.Filtered", paste(
dim(sobj),
collapse = '.'
)
), collapse = "_"),
".RDS")
## Check
print(out_name)Show output
[1] "/shared/projects/ebaii_sc_teachers/SC_TD/03_Preproc.3/RESULTS/05_TD3A_S5_Doublets.Filtered_12508.4035.RDS"
12 Rsession
Show output
R version 4.4.1 (2024-06-14)
Platform: x86_64-conda-linux-gnu
Running under: Ubuntu 22.04.5 LTS
Matrix products: default
BLAS/LAPACK: /shared/ifbstor1/software/miniconda/envs/r-4.4.1/lib/libopenblasp-r0.3.29.so; LAPACK version 3.12.0
locale:
[1] LC_CTYPE=en_US.UTF-8 LC_NUMERIC=C
[3] LC_TIME=en_US.UTF-8 LC_COLLATE=en_US.UTF-8
[5] LC_MONETARY=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8
[7] LC_PAPER=en_US.UTF-8 LC_NAME=C
[9] LC_ADDRESS=C LC_TELEPHONE=C
[11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C
time zone: Europe/Paris
tzcode source: system (glibc)
attached base packages:
[1] stats graphics grDevices utils datasets methods base
other attached packages:
[1] SeuratObject_5.1.0 sp_2.2-0
loaded via a namespace (and not attached):
[1] RcppAnnoy_0.0.22 splines_4.4.1
[3] later_1.4.2 BiocIO_1.14.0
[5] bitops_1.0-9 tibble_3.2.1
[7] polyclip_1.10-7 pROC_1.18.5
[9] XML_3.99-0.18 fastDummies_1.7.5
[11] lifecycle_1.0.4 scDblFinder_1.18.0
[13] edgeR_4.2.2 globals_0.18.0
[15] lattice_0.22-6 MASS_7.3-65
[17] magrittr_2.0.3 limma_3.60.6
[19] plotly_4.10.4 sass_0.4.10
[21] rmarkdown_2.29 jquerylib_0.1.4
[23] yaml_2.3.10 metapod_1.12.0
[25] httpuv_1.6.15 Seurat_5.3.0
[27] sctransform_0.4.2 spam_2.11-1
[29] spatstat.sparse_3.1-0 reticulate_1.42.0
[31] cowplot_1.1.3 pbapply_1.7-2
[33] RColorBrewer_1.1-3 abind_1.4-8
[35] zlibbioc_1.50.0 Rtsne_0.17
[37] GenomicRanges_1.56.2 purrr_1.0.4
[39] RCurl_1.98-1.17 BiocGenerics_0.50.0
[41] GenomeInfoDbData_1.2.12 IRanges_2.38.1
[43] S4Vectors_0.42.1 ggrepel_0.9.6
[45] irlba_2.3.5.1 listenv_0.9.1
[47] spatstat.utils_3.1-4 goftest_1.2-3
[49] RSpectra_0.16-2 dqrng_0.4.1
[51] spatstat.random_3.4-1 fitdistrplus_1.2-2
[53] parallelly_1.45.0 DelayedMatrixStats_1.26.0
[55] codetools_0.2-20 DelayedArray_0.30.1
[57] scuttle_1.14.0 tidyselect_1.2.1
[59] UCSC.utils_1.0.0 farver_2.1.2
[61] viridis_0.6.5 ScaledMatrix_1.12.0
[63] matrixStats_1.5.0 stats4_4.4.1
[65] rmdformats_1.0.4 spatstat.explore_3.4-3
[67] GenomicAlignments_1.40.0 jsonlite_2.0.0
[69] BiocNeighbors_1.22.0 progressr_0.15.1
[71] scater_1.32.1 ggridges_0.5.6
[73] survival_3.7-0 tools_4.4.1
[75] ica_1.0-3 Rcpp_1.0.14
[77] glue_1.8.0 gridExtra_2.3
[79] SparseArray_1.4.8 xfun_0.52
[81] MatrixGenerics_1.16.0 GenomeInfoDb_1.40.1
[83] dplyr_1.1.4 withr_3.0.2
[85] fastmap_1.2.0 bluster_1.14.0
[87] digest_0.6.37 rsvd_1.0.5
[89] R6_2.6.1 mime_0.13
[91] scattermore_1.2 tensor_1.5
[93] dichromat_2.0-0.1 spatstat.data_3.1-6
[95] UpSetR_1.4.0 tidyr_1.3.1
[97] generics_0.1.4 data.table_1.17.4
[99] rtracklayer_1.64.0 httr_1.4.7
[101] htmlwidgets_1.6.4 S4Arrays_1.4.1
[103] uwot_0.2.3 pkgconfig_2.0.3
[105] gtable_0.3.6 lmtest_0.9-40
[107] SingleCellExperiment_1.26.0 XVector_0.44.0
[109] htmltools_0.5.8.1 dotCall64_1.2
[111] bookdown_0.39 scales_1.4.0
[113] Biobase_2.64.0 png_0.1-8
[115] spatstat.univar_3.1-3 scran_1.32.0
[117] knitr_1.50 rstudioapi_0.17.1
[119] rjson_0.2.23 reshape2_1.4.4
[121] curl_6.2.3 nlme_3.1-165
[123] cachem_1.1.0 zoo_1.8-14
[125] stringr_1.5.1 KernSmooth_2.23-24
[127] parallel_4.4.1 miniUI_0.1.2
[129] vipor_0.4.7 restfulr_0.0.15
[131] ggrastr_1.0.2 pillar_1.10.2
[133] grid_4.4.1 vctrs_0.6.5
[135] RANN_2.6.2 promises_1.3.2
[137] BiocSingular_1.20.0 beachmat_2.20.0
[139] xtable_1.8-4 cluster_2.1.6
[141] beeswarm_0.4.0 scds_1.20.0
[143] evaluate_1.0.3 locfit_1.5-9.9
[145] cli_3.6.5 compiler_4.4.1
[147] Rsamtools_2.20.0 rlang_1.1.6
[149] crayon_1.5.3 future.apply_1.11.3
[151] labeling_0.4.3 plyr_1.8.9
[153] ggbeeswarm_0.7.2 stringi_1.8.7
[155] viridisLite_0.4.2 deldir_2.0-4
[157] BiocParallel_1.38.0 Biostrings_2.72.1
[159] lazyeval_0.2.2 spatstat.geom_3.4-1
[161] Matrix_1.7-3 RcppHNSW_0.6.0
[163] patchwork_1.3.0 sparseMatrixStats_1.16.0
[165] future_1.49.0 ggplot2_3.5.2
[167] statmod_1.5.0 shiny_1.10.0
[169] SummarizedExperiment_1.34.0 ROCR_1.0-11
[171] igraph_2.1.4 bslib_0.9.0
[173] xgboost_1.7.11.1